package org.summerframework.web.exception.resolver;

import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import org.summerframework.core.base.result.DefaultResultCode;
import org.summerframework.core.base.result.ResultCode;
import org.summerframework.core.base.result.ResultDTO;
import org.summerframework.core.exception.AppException;
import org.summerframework.core.util.SpringContextUtil;
import org.summerframework.core.util.UrlUtil;
import org.summerframework.web.view.DefaultMappingJackson2JsonView;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.ConversionNotSupportedException;
import org.springframework.beans.TypeMismatchException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.validation.BindException;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingPathVariableException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.multipart.support.MissingServletRequestPartException;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.sql.SQLException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

/**
 * 使用示例1:产生异常后默认进入jsonErr模版,如需进入htmlErr模版则在controller中添加HtmlRequired注解
 * <pre>
 * <bean id="simpleMappingExceptionResolver" class="org.summerframework.web.exception.resolver.UriExposeExceptionResolver">
 * <property name="classes">
 * <list value-type="java.lang.Class[]">
 * <value type="java.lang.Class">org.summerframework.web.annotation.HtmlRequired</value>
 * </list>
 * </property>
 * <property name="annotationErrorView" value="exception/htmlErr"/>
 * <property name="defaultErrorView" value="exception/jsonErr"/>
 * <property name="order" value="0" />
 * </bean>
 * </pre>
 * <p>
 * 使用示例2:默认进入htmlErr模版
 * <pre>
 * <bean id="simpleMappingExceptionResolver" class="org.summerframework.web.exception.resolver.UriExposeExceptionResolver">
 * <property name="classes">
 * <list value-type="java.lang.Class[]">
 * <value type="java.lang.Class">org.summerframework.web.annotation.JsonRequired</value>
 * <value type="java.lang.Class">org.springframework.web.bind.annotation.ResponseBody</value>
 * </list>
 * </property>
 * <property name="annotationErrorView" value="exception/jsonErr"/>
 * <property name="defaultErrorView" value="exception/htmlErr"/>
 * <property name="order" value="0" />
 * </bean>
 * </pre>
 *
 * @author shichao 2017年7月12日
 */
@Getter
@Setter
public class UriExposeExceptionResolver extends SimpleMappingExceptionResolver {
    private Logger logger = LoggerFactory.getLogger(getClass());
    private String annotationErrorView;

    private Class<Annotation>[] classes;

    private Map<String, String> exceptionResultCode;

    private Map<Class<? extends Exception>, ResultCode> resultCodeMap = new LinkedHashMap<>();

    @Autowired(required = false)
    private List<ExceptionResolveHandler> exceptionResolveHandlerList;

    private boolean defaultJson = false;

    @PostConstruct
    public void init() {
        if (exceptionResultCode != null) {
            exceptionResultCode.forEach((key, resultCode) -> {
                try {
                    Class<?> aClass = Class.forName(key);
                    int end = StringUtils.lastIndexOf(resultCode, ".");
                    Class<?> forName = Class.forName(StringUtils.substring(resultCode, 0, end));

                    Method valueOf = forName.getMethod("valueOf", String.class);
                    ResultCode resultCode1 = (ResultCode) valueOf.invoke(forName, StringUtils.substring(resultCode, end + 1, resultCode.length()));

                    if (Exception.class.isAssignableFrom(aClass)) {
                        resultCodeMap.put((Class<? extends Exception>) aClass, resultCode1);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }
    }

    @Override
    protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler,
                                              Exception ex) {
        logger.error("产生异常:", ex);
        ExceptionResolveHandler exceptionResolveHandler = null;

        if (CollectionUtils.isNotEmpty(exceptionResolveHandlerList)) {
            for (ExceptionResolveHandler temp : exceptionResolveHandlerList) {
                try {
                    if (temp.supper(ex)) {
                        exceptionResolveHandler = temp;
                        break;
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }

        ResultCode resultCode = resultCodeMap.get(ex.getClass());
        ResultDTO resultDTO = null;

        if (resultCode == null && exceptionResolveHandler != null) {
            resultCode = exceptionResolveHandler.resolveException(request, response, handler, ex);
        }

        if (resultCode == null) {
            if (ex instanceof AppException) {
                AppException webException = (AppException) ex;
                if (webException.getResultCode() != null) {
                    resultCode = webException.getResultCode();
                    resultDTO = webException.getResultDTO();
                } else {
                    resultCode = DefaultResultCode.SYSTEM_ERROR;
                }
            } else {
                Optional<Class<? extends Exception>> first = resultCodeMap.keySet().stream().filter(e -> e.isAssignableFrom(ex.getClass())).findFirst();

                if (first.isPresent()) {
                    resultCode = resultCodeMap.get(first.get());
                } else if (ex instanceof HttpRequestMethodNotSupportedException) {
                    resultCode = DefaultResultCode.ERROR_REQUEST_METHOD_NOT_ALLOWED;
                } else if (ex instanceof HttpMediaTypeNotSupportedException) {
                    resultCode = DefaultResultCode.ERROR_REQUEST_UNSUPPORTED_MEDIA_TYPE;
                } else if (ex instanceof HttpMediaTypeNotAcceptableException) {
                    resultCode = DefaultResultCode.ERROR_REQUEST_NOT_ACCEPTABLE;
                } else if (ex instanceof MissingPathVariableException) {
                    resultCode = DefaultResultCode.ERROR_REQUEST_INTERNAL_SERVER_ERROR;
                } else if (ex instanceof MissingServletRequestParameterException) {
                    resultCode = DefaultResultCode.ERROR_REQUEST_MISSING_PARAM;
                } else if (ex instanceof ServletRequestBindingException) {
                    resultCode = DefaultResultCode.ERROR_REQUEST_BAD_REQUEST;
                } else if (ex instanceof ConversionNotSupportedException) {
                    resultCode = DefaultResultCode.ERROR_REQUEST_INTERNAL_SERVER_ERROR;
                } else if (ex instanceof TypeMismatchException) {
                    resultCode = DefaultResultCode.ERROR_REQUEST_BAD_REQUEST;
                } else if (ex instanceof HttpMessageNotReadableException) {
                    resultCode = DefaultResultCode.ILLEGAL_ARGUMENT;
                } else if (ex instanceof HttpMessageNotWritableException) {
                    resultCode = DefaultResultCode.ERROR_REQUEST_BAD_REQUEST;
                } else if (ex instanceof MethodArgumentNotValidException) {
                    resultCode = DefaultResultCode.ERROR_REQUEST_BAD_REQUEST;
                } else if (ex instanceof MissingServletRequestPartException) {
                    resultCode = DefaultResultCode.ERROR_REQUEST_BAD_REQUEST;
                } else if (ex instanceof BindException) {
                    resultCode = DefaultResultCode.ERROR_REQUEST_BAD_REQUEST;
                } else if (ex instanceof NoHandlerFoundException) {
                    resultCode = DefaultResultCode.ERROR_REQUEST_NOT_FIND;
                } else if (ex instanceof InvalidFormatException) {
                    resultCode = DefaultResultCode.ILLEGAL_ARGUMENT;
                } else if (ex instanceof SQLException) {
                    resultCode = DefaultResultCode.DAO_ERROR;
                } else if (ex instanceof RuntimeException) {
                    resultCode = DefaultResultCode.RUNTIME_ERROR;
                } else {
                    resultCode = DefaultResultCode.SYSTEM_ERROR;
                }

                resultCodeMap.put(ex.getClass(), resultCode);
            }

        }

        if (resultDTO == null) {
            resultDTO = ResultDTO.error(resultCode);
        }


        boolean isHtml = !defaultJson && !(handler instanceof HandlerMethod && hasAnnotation((HandlerMethod) handler, classes));

        if (exceptionResolveHandler != null) {
            ModelAndView mv = exceptionResolveHandler.determineModelAndView(request, resultDTO, isHtml, ex);

            if (mv != null) {
                return mv;
            }
        }

        String viewName = determineViewName(ex, request);

        Integer statusCode = determineStatusCode(request, viewName);
        if (statusCode != null) {
            applyStatusCodeIfPossible(request, response, statusCode);
        }

        ModelAndView mv;

        if (isHtml) {
            mv = getModelAndView(viewName, ex, request);
            mv.addObject("result", resultDTO);
            mv.addObject("urlUtil", UrlUtil.getUrl());
        } else {
            mv = new ModelAndView(SpringContextUtil.getBean(DefaultMappingJackson2JsonView.class));
            mv.addObject(resultDTO);
        }

        return mv;
    }

    private final boolean hasAnnotation(HandlerMethod handlerMethod, Class<? extends Annotation>... annotations) {
        for (Class<? extends Annotation> annotation : annotations) {
            if (AnnotationUtils.findAnnotation(handlerMethod.getMethod(), annotation) != null
                    || AnnotationUtils.findAnnotation(handlerMethod.getBean().getClass(), annotation) != null) {
                return true;
            }
        }

        return false;
    }
}
